/**
 * 
 */
package se.zens.common;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;

import se.zens.common.exception.ConfigurationException;

/**
 * A property list just like java.util.Properties except for a prefix which is
 * set at construction. If a parameter is not found a new search is then
 * automatically made with the prefix attached to the key.
 */
public class PrefixProperties extends Properties
{
	static SimpleDateFormat dateFormat = new SimpleDateFormat(
			"yyyy-MM-dd HH:mm");

	protected String prefix;

	/**
	 * Creates a new object with no prefix.
	 * 
	 */
	public PrefixProperties()
	{
		super();
		prefix = "";
	}

	/**
	 * Creates a new object with the given prefix.
	 * 
	 * @param prefix
	 *            The prefix for these properties
	 * 
	 */
	public PrefixProperties(String prefix)
	{
		super();
		prefix = prefix;
	}

	/**
	 * Creates a new object with empty prefix and given default properties.
	 * 
	 * @param def
	 *            The default properties.
	 * 
	 */
	public PrefixProperties(Properties def)
	{
		super(def);
		prefix = "";
	}

	/**
	 * Creates a new object with the given prefix and default properties.
	 * 
	 * @param def
	 *            The default properties.
	 * 
	 */
	public PrefixProperties(String prefix, Properties def)
	{
		super(def);
		this.prefix = prefix;
	}

	/**
	 * Returns a new extended properties object 
	 * with the given prefix appended to the prefix of this
	 * extended properties prefix
	 * 
	 * @param prefix The prefix to append
	 * @return The new extended properties object
	 */
	public
	PrefixProperties getSubProperties(String prefix)
	{
		return new PrefixProperties(prefix, this);
	}
	
	/**
	 * Searches for the property with the given key using the property list
	 * prefix. The property list hierarchy is recursively searched with its
	 * corresponding property list prefixes appended.
	 * 
	 * @param key
	 *            The key.
	 * @return The value if found, otherwise null.
	 * 
	 */
	@Override
	public String getProperty(String key)
	{
		String value = super.getProperty(prefix + key);

		if (value != null)
		{
			value = value.trim();
		}
		return value;
	}

	@Override
	public Object get(Object key)
	{
		return super.get(prefix + (String) key);
	}

	/**
	 * Searches for the property with the given key containing a date using the
	 * property list prefix. The property list hierarchy is recursively searched
	 * with its corresponding property list prefixes appended.
	 * 
	 * @param key
	 *            The key.
	 * @param defaultValue
	 *            The default value for non-existing key.
	 * @param mandatory
	 *            The value must exist or not.
	 * @return The value if found, otherwise the default value if non-mandatory.
	 * @throws ERSConfigurationException
	 *             If mandatory is true and key is not found
	 */
	public Date getDateProperty(String key, Date defaultValue, boolean mandatory)
			throws ConfigurationException
	{
		try
		{
			String value = getProperty(key);
			if (value != null)
				return dateFormat.parse(value);
			if (mandatory)
				throw new ConfigurationException("Missing property "
						+ getFullPropertyKey(key));
			return defaultValue;
		}
		catch (ParseException e)
		{
			throw new ConfigurationException(
					"Invalid date format of property \"" + key + "!");
		}
	}

	/**
	 * Searches for the property with the given key containing a boolean value.
	 * The string 'TRUE' in any case is interpreted as true. The string 'FALSE'
	 * in any case is interpreted as false. Anything else will result in the default
	 * value being returned.
	 * 
	 * @param key
	 *            The key to search for.
	 * @param defaultValue
	 *            The default value for non-existing key or incorrect value.
	 * @param mandatory
	 *            If the value must exist or not.
	 * @return The value if found, otherwise the default value if non-mandatory.
	 * @throws ERSConfigurationException
	 *             If mandatory is true and key is not found
	 */
	public boolean getBooleanProperty(String key, boolean defaultValue, boolean mandatory)
	throws ConfigurationException
	{
		String val = getProperty(key);
		
		if (val != null)
		{
			if (val.equalsIgnoreCase("TRUE"))
			{
				return true;
			}
			if (val.equalsIgnoreCase("FALSE"))
			{
				return false;
			}
			else if (mandatory)
			{
				throw new ConfigurationException("Boolean property "
						+ getFullPropertyKey(key) + " is neither 'true' nor 'false'");
			}
		}
		else if (mandatory)
		{
			throw new ConfigurationException("Missing property "
					+ getFullPropertyKey(key));
		}
		
		return defaultValue;
	}
	
	/**
	 * Searches for the property with the given key containing a number using
	 * the property list prefix. The property list hierarchy is recursively
	 * searched with its corresponding property list prefixes appended. Numbers
	 * can also be specified in the properties with suffixes: K (thousand), M
	 * (millions), G (billions)
	 * 
	 * @param key
	 *            The key.
	 * @param defaultValue
	 *            The default value for non-existing key.
	 * @param mandatory
	 *            The value must exist or not.
	 * @return The value if found, otherwise the default value if non-mandatory.
	 * @throws ERSConfigurationException
	 *             If mandatory is true and key is not found
	 */
	public long getNumberProperty(String key, long defaultValue,
			boolean mandatory) throws ConfigurationException
	{
		try
		{
			String valueStr = getProperty(key);
			if (valueStr == null)
			{
				if (mandatory)
				{
					throw new ConfigurationException("Missing property "
							+ getFullPropertyKey(key));
				}
				return defaultValue;
			}

			valueStr = valueStr.trim();
			long multiplier = 1;
			for (int i = 0; i < valueStr.length(); ++i)
			{
				if (!Character.isDigit(valueStr.charAt(i)))
				{
					String suffix = valueStr.substring(i).trim().toUpperCase();
					valueStr = valueStr.substring(0, i);
					switch (suffix.charAt(0))
					{
					case 'K':
						multiplier = 1000;
						break;
					case 'M':
						multiplier = 1000000;
						break;
					case 'G':
						multiplier = 1000000000;
						break;
					}
					break;
				}
			}
			long value = Long.parseLong(valueStr) * multiplier;
			return value;
		}
		catch (NumberFormatException e)
		{
			throw new ConfigurationException(
					"Invalid number format of property \"" + key + "!");
		}
	}

	/**
	 * Searches for the property with the given key containing an integer using
	 * the property list prefix. The property list hierarchy is recursively
	 * searched with its corresponding property list prefixes appended. Numbers
	 * can also be specified in the properties with suffixes: K (thousand), M
	 * (millions), G (billions)
	 * 
	 * @param key
	 *            The key.
	 * @param defaultValue
	 *            The default value for non-existing key.
	 * @param mandatory
	 *            The value must exist or not.
	 * @return The value if found, otherwise the default value if non-mandatory.
	 * @throws ERSConfigurationException
	 *             If mandatory is true and key is not found
	 */
	public Integer getIntegerProperty(String key, Integer defaultValue,
			boolean mandatory) throws ConfigurationException
	{
		try
		{
			String valueStr = getProperty(key);
			if (valueStr == null)
			{
				if (mandatory)
				{
					throw new ConfigurationException("Missing property "
							+ getFullPropertyKey(key));
				}
				return defaultValue;
			}

			valueStr = valueStr.trim();
			int multiplier = 1;
			for (int i = 0; i < valueStr.length(); ++i)
			{
				if (!Character.isDigit(valueStr.charAt(i)))
				{
					String suffix = valueStr.substring(i).trim().toUpperCase();
					valueStr = valueStr.substring(0, i);
					switch (suffix.charAt(0))
					{
					case 'K':
						multiplier = 1000;
						break;
					case 'M':
						multiplier = 1000000;
						break;
					case 'G':
						multiplier = 1000000000;
						break;
					}
					break;
				}
			}
			return new Integer(Integer.parseInt(valueStr) * multiplier);
		}
		catch (NumberFormatException e)
		{
			throw new ConfigurationException(
					"Invalid number format of property \"" + key + "!");
		}
	}

	/**
	 * Searches for the property with the given key containing a decimal number
	 * using the property list prefix. The property list hierarchy is
	 * recursively searched with its corresponding property list prefixes
	 * appended.
	 * 
	 * @param key
	 *            The key.
	 * @param defaultValue
	 *            The default value for non-existing key.
	 * @param mandatory
	 *            The value must exist or not.
	 * @return The value if found, otherwise the default value if non-mandatory.
	 * @throws ERSConfigurationException
	 *             If mandatory is true and key is not found
	 */
	public BigDecimal getDecimalProperty(String key, BigDecimal defaultValue,
			boolean mandatory) throws ConfigurationException
	{
		try
		{
			String valueStr = getProperty(key);
			if (valueStr == null)
			{
				if (mandatory)
				{
					throw new ConfigurationException("Missing property "
							+ getFullPropertyKey(key));
				}
				return defaultValue;
			}

			return new BigDecimal(valueStr);
		}
		catch (NumberFormatException e)
		{
			throw new ConfigurationException(
					"Invalid number format of property \"" + key + "!");
		}
	}
	
	
	/**
	 * Searches for the property with the given key containing a duration in
	 * milliseconds using the property list prefix. The property list hierarchy
	 * is recursively searched with its corresponding property list prefixes
	 * appended. Numbers can also be specified in the properties with suffixes:
	 * S/s (seconds), H/h (thousand), M/m (minutes), D/d (days)
	 * 
	 * @param key
	 *            The key.
	 * @param defaultValue
	 *            The default value for non-existing key.
	 * @param mandatory
	 *            The value must exist or not.
	 * @return The value if found, otherwise the default value if non-mandatory.
	 * @throws ERSConfigurationException
	 *             If mandatory is true and key is not found
	 */
	public Long getDurationProperty(String key, Long defaultValue,
			boolean mandatory) throws ConfigurationException
	{
		try
		{
			String valueStr = getProperty(key);
			if (valueStr == null)
			{
				if (mandatory)
				{
					throw new ConfigurationException("Missing property "
							+ getFullPropertyKey(key));
				}
				return defaultValue;
			}
			valueStr = valueStr.trim();
			long multiplier = 1;
			for (int i = 0; i < valueStr.length(); ++i)
			{
				if (!Character.isDigit(valueStr.charAt(i)))
				{
					String suffix = valueStr.substring(i).trim().toUpperCase();
					valueStr = valueStr.substring(0, i);
					switch (suffix.charAt(0))
					{
					case 'S':
						multiplier = 1000;
						break;
					case 'M':
						multiplier = 60000;
						break;
					case 'H':
						multiplier = 3600000;
						break;
					case 'D':
						multiplier = 86400000;
						break;
					}
					break;
				}
			}
			long value = Long.parseLong(valueStr) * multiplier;
			return value;
		}
		catch (NumberFormatException e)
		{
			throw new ConfigurationException(
					"Invalid number format of property \"" + key + "!");
		}
	}

	/**
	 * Searches for the property with the given key using the property list
	 * prefix. The property list hierarchy is recursively searched with its
	 * corresponding property list prefixes appended.
	 * 
	 * @param key
	 *            The key.
	 * @param defaultValue
	 *            The default value for non-existing key.
	 * @param mandatory
	 *            The value must exist or not.
	 * @return The value if found, otherwise the default value if non-mandatory.
	 * @throws ERSConfigurationException
	 *             If mandatory is true and key is not found
	 */
	public String getProperty(String key, String defaultValue, boolean mandatory)
			throws ConfigurationException
	{
		String value = getProperty(key);
		if (value != null)
			return value.trim();
		if (mandatory)
			throw new ConfigurationException("Missing property "
					+ getFullPropertyKey(key));
		return defaultValue;
	}

	/**
	 * Creates an array of the value that is split according to split delimitter
	 * and trimming.
	 * 
	 * @param key
	 *            The key.
	 * @param defaultValue
	 *            The default value for non-existing key.
	 * @param splitRegexp
	 *            The split regular expression.
	 * @param trim
	 *            true if each value in the splitted string should be trimmed.
	 * @param mandatory
	 *            The value must exist or not.
	 * @return The map of values if found, otherwise the default value if
	 *         not-mandatory.
	 * @throws ERSConfigurationException
	 *             If mandatory is true and key is not found.
	 */
	public Map<String, String> getListProperty(String key,
			Map<String, String> defaultValue, String splitRegexp, boolean trim,
			boolean mandatory) throws ConfigurationException
	{
		String value = getProperty(key, null, mandatory);
		if (value == null)
		{
			return defaultValue;
		}

		LinkedHashMap<String, String> result = new LinkedHashMap<String, String>();

		for (String keyValue : value.split(splitRegexp))
		{
			if (trim)
			{
				keyValue = keyValue.trim();
			}

			if (keyValue != null && keyValue.length() > 0)
			{
				result.put(keyValue, keyValue);
			}
		}

		return result;

	}

	/**
	 * Generates the full distinguished name for a given key for the complete
	 * hierarchy with corresponding prefixes.
	 * 
	 * @param key
	 *            The given key.
	 * @return The full distinguished name.
	 */
	public String getFullPropertyKey(String key)
	{
		return getFullPropertyKeyRecursive(key, "");
	}

	/**
	 * Help method to recursively generate a full distinguished name for a given
	 * key.
	 * 
	 * @param key
	 *            The given key.
	 * @param keyTrace
	 *            The given key trace.
	 * @return The full distinguished name.
	 */
	private String getFullPropertyKeyRecursive(String key, String keyTrace)
	{
		if (defaults != null && defaults instanceof PrefixProperties)
		{
			return ((PrefixProperties) defaults).getFullPropertyKeyRecursive(
					key, prefix + keyTrace);
		}

		return prefix + keyTrace + key;
	}

	private String getFullPrefix()
	{
		if (defaults != null && defaults instanceof PrefixProperties)
		{
			return ((PrefixProperties) defaults).getFullPrefix() + prefix;
		}

		return prefix;

	}

	/**
	 * Searches for the property with the given key using the property list
	 * prefix and the given default value if the key is not found. The property
	 * list hierarchy is recursively searched with its corresponding property
	 * list prefixes appended.
	 * 
	 * @param key The given key.
	 * 
	 * @param def The given default value for non existing key.
	 * 
	 * @return The value if existing, otherwise the default value.
	 */
	@Override
	public String getProperty(String key, String def)
	{
		String value = super.getProperty(key, def);

		if (value != null)
		{
			value = value.trim();
		}
		return value;

	}

	/**
	 * Set a property for the given key and value. The property list prefix is
	 * automatically appended.
	 * 
	 * @param key The given key.
	 * 
	 * @param value The given value.
	 */
	@Override
	public synchronized Object setProperty(String key, String value)
	{
		return super.setProperty(prefix + key, value);
	}

	/**
	 * Set a property for the given key and value. The property list prefix is
	 * automatically appended.
	 * 
	 * @param key The given key.
	 * 
	 * @param value The given value.
	 */
	public synchronized Object setDefaultProperty(String key, String value)
	{
		String currentValue = getProperty(key);
		if (currentValue == null)
			return setProperty(key, value);
		return currentValue;
	}

	/**
	 * gets the lists prefix.
	 * 
	 * @return A prefix.
	 * 
	 */
	public String getPrefix()
	{
		return prefix;
	}

	/**
	 * Load properties from file.
	 * 
	 * @param file_path
	 *            The given file.
	 * @throws IOException
	 *             at file error.
	 */
	public void loadFromFile(String file_path) throws IOException
	{
		FileInputStream file_stream = new FileInputStream(file_path);

		load(file_stream);
	}

	/**
	 * Load properties from file with a given encoding.
	 * 
	 * @param file_path
	 *            The given file.
	 * @param encoding
	 *            The given encoding.
	 * @throws IOException
	 *             at file error.
	 */
	public void loadFromFile(String file_path, String encoding)
			throws IOException
	{
		InputStream stream = new FileInputStream(file_path);
		loadFromStream(stream, encoding);
	}

	/**
	 * Load properties from file with a given encoding.
	 * 
	 * @param file
	 *            The given file handle.
	 * @param encoding
	 *            The given encoding.
	 * @throws IOException
	 *             at file error.
	 */
	public void loadFromFile(File file, String encoding) throws IOException
	{
		InputStream stream = new FileInputStream(file);
		loadFromStream(stream, encoding);
	}

	/**
	 * Load properties from file.
	 * 
	 * @param file
	 *            The given file handle.
	 * @throws IOException
	 *             at file error.
	 */
	public void loadFromFile(File file) throws IOException
	{
		InputStream stream = new FileInputStream(file);
		load(stream);
	}

	/**
	 * Load properties from an input stream.
	 * 
	 * @param stream
	 *            The given input stream.
	 * @param encoding
	 *            The given encoding.
	 * @throws IOException
	 *             at file error.
	 */
	public void loadFromStream(InputStream stream, String encoding)
			throws IOException
	{

		BufferedReader in = new BufferedReader(new InputStreamReader(stream,
				encoding));

		while (true)
		{
			String line = in.readLine();
			if (line == null)
				break;
			line = line.trim();
			if (line.length() == 0)
				continue;

			int eqpos = line.indexOf("=");
			if (eqpos > 0)
			{
				String id = line.substring(0, eqpos).trim();
				String value = line.substring(eqpos + 1).trim();
				setProperty(id, value);
			}
		}
	}

	/**
	 * Save properties to file.
	 * 
	 * @param props
	 *            The given property list.
	 * @param file_path
	 *            The given file.
	 * @throws IOException
	 *             at file error.
	 */
	public static void saveToPropertiesFile(Properties props, String file_path)
			throws IOException
	{

		FileOutputStream file_stream = new FileOutputStream(file_path);

		props.store(file_stream, null);
		
		file_stream.close();

	}

	/**
	 * Load properties from a given string.
	 * 
	 * @param string
	 *            The given string.
	 * @throws IOException
	 *             at error.
	 */
	public void loadFromString(String string) throws IOException
	{
		ByteArrayInputStream stream = new 
			ByteArrayInputStream(string.getBytes());

		load(stream);
	}

	/**
	 * Save properties to a string.
	 * 
	 * @return The formatted property string.
	 * @throws IOException
	 *             at error.
	 */
	public String saveToString() throws IOException
	{
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		store(out, "");

		return out.toString();
	}

	public class PrefixedKeyEnumeration implements Enumeration<Object>
	{
		Enumeration<?> parentEnum;

		String nextElement = null;

		String fullPrefix;

		public PrefixedKeyEnumeration(Enumeration<?> parentEnum)
		{
			this.parentEnum = parentEnum;
			fullPrefix = getFullPrefix();
		}

		public boolean hasMoreElements()
		{
			return fetchNextElement();
		}

		public Object nextElement()
		{
			if (fetchNextElement())
			{
				String element = nextElement;
				nextElement = null;
				return element;
			}
			return null;
		}

		boolean fetchNextElement()
		{
			if (nextElement != null)
				return true;
			while (parentEnum.hasMoreElements())
			{
				String element = (String) parentEnum.nextElement();
				if (element.startsWith(fullPrefix))
				{
					nextElement = element.substring(fullPrefix.length());
					return true;
				}
			}

			return false;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized Enumeration<Object> keys()
	{
		return new PrefixedKeyEnumeration(super.keys());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized Enumeration<Object> propertyNames()
	{
		return new PrefixedKeyEnumeration(super.propertyNames());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Set<Object> keySet()
	{
		TreeSet<Object> set = new TreeSet<Object>();

		Enumeration<Object> names = propertyNames();
		while (names.hasMoreElements())
		{
			Object key = names.nextElement();
			set.add(key);
		}
		return set;
	}

	/**
	 * Truncates the property keys after the specified delimiter.
	 * 
	 * Example. Consider the property keys: 1.key, 2.key, 3.key
	 * 
	 * A call to the truncatedKeys method with delimiter . will return: 1, 2, 3
	 * 
	 * @param delimiter
	 *            delimiter character
	 * @return a set with the truncated keys
	 */
	public Set<String> truncatedKeys(String delimiter)
	{
		Set<String> set = new TreeSet<String>();

		Enumeration<Object> names = propertyNames();
		while (names.hasMoreElements())
		{
			String fullKey = (String) names.nextElement();
			int dp = fullKey.indexOf(delimiter);
			String key = null;
			if (dp > 0)
				key = fullKey.substring(0, dp);
			else
				key = fullKey;
			if (!set.contains(key))
			{
				set.add(key);
			}
		}
		return set;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String toString()
	{
		StringBuffer sb = new StringBuffer("{");

		for (Object o : keySet())
		{
			String key = (String) o;
			sb.append(key + "=" + getProperty(key) + ", ");
		}
		if (sb.length() > 2)
		{
			sb.deleteCharAt(sb.length() - 1);
			sb.deleteCharAt(sb.length() - 1);
		}
		sb.append("}");

		return sb.toString();
	}

	/**
	 * Converts this ExtendedProperties object as a Properties object.
	 * 
	 * Note: Filters out any properties that do not have the given prefix.
	 * 
	 * @return Return a properties for use in other programs that don't
	 *         understand ExtendedProperties.
	 */
	public Properties asProperties()
	{
		Properties properties = new Properties();

		for (Object key : keySet())
		{

			properties.setProperty((String) key, this.getProperty((String) key));

		}
		return properties;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized boolean containsKey(Object key)
	{
		//just calling super class containsKey did not work,
		//but this logic should not be different anyway
		return super.getProperty(prefix + key) != null;
	}

	
}
